package ckw.communication;

import java.net.BindException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

import javax.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import ckw.database.DatabaseService;
import ckw.database.model.AdministratorToken;
import ckw.database.model.Election;
import ckw.database.model.ElectionChanges;
import ckw.database.model.ElectionNode;
import ckw.database.model.ElectionNonResidentVoter;
import ckw.database.model.ElectionResidentVoter;
import ckw.database.model.Member;
import ckw.database.model.MessageReceived;
import ckw.database.model.MessageToSend;
import ckw.database.model.Node;
import ckw.database.model.NodeChanges;
import ckw.database.model.Voter;
import ckw.election.ElectionService;
import ckw.gui.MainWindow;
import ckw.node.NodesService;
import core.communication.message.CNodeState;
import core.communication.message.ConfigurationMessage;
import core.communication.message.ElectionConfigurationMessage;
import core.communication.message.ElectionStatusMessage;
import core.communication.message.Message;
import core.communication.message.MessagePriorityQueue;
import core.communication.message.NodeStateEnum;
import core.communication.message.UpdateMemberMessage;
import core.communication.message.UpdateVoterMessage;
import core.communication.message.VerifyNonresidentVoterConfirmationMessage;
import core.communication.message.VerifyNonresidentVoterMessage;
import core.communication.message.VotersPackageMessage;
import core.communication.message.VotesPackageMessage;
import core.database.ElectionStatus;
import core.utils.Converter;

@Service
public class CommunicationServiceImpl implements CommunicationService {

	private static final Logger L = LoggerFactory.getLogger(CommunicationServiceImpl.class);

	private DatabaseService databaseService;
	private Server server;
	private NodesService nodesService;
	private MessageSender messageSender;
	private MessageReceiver messageReceiver;
	private ClientChecker clientChecker;
	private CommunicationChecker communicationChecker;
	private MessagePriorityQueue messagesToSend = new MessagePriorityQueue();
	private MessagePriorityQueue messagesReceived = new MessagePriorityQueue();
	private LinkedList<Client> clientsToCheck = new LinkedList<Client>();
	private HashMap<String, Client> clients = new HashMap<String, Client>();
	private Node mainNode;
	private MainWindow mainWindow;
	private ThreadPoolTaskExecutor taskExecutor;
	private ElectionService electionService;

	@Inject
	public CommunicationServiceImpl(DatabaseService databaseService, Server server, NodesService nodesService, MessageSender messageSender, MessageReceiver messageReceiver,
			ClientChecker clientChecker, CommunicationChecker communicationChecker, MainWindow mainWindow, ThreadPoolTaskExecutor taskExecutor, ElectionService electionService) {
		L.debug("Tworzę CommunicationService");

		Assert.notNull(databaseService);
		Assert.notNull(server);
		Assert.notNull(nodesService);
		Assert.notNull(messageSender);
		Assert.notNull(messageReceiver);
		Assert.notNull(clientChecker);
		Assert.notNull(communicationChecker);
		Assert.notNull(mainWindow);
		Assert.notNull(taskExecutor);
		Assert.notNull(electionService);

		this.databaseService = databaseService;
		this.server = server;
		this.nodesService = nodesService;
		this.messageSender = messageSender;
		this.messageReceiver = messageReceiver;
		this.clientChecker = clientChecker;
		this.communicationChecker = communicationChecker;
		this.mainWindow = mainWindow;
		this.taskExecutor = taskExecutor;
		this.electionService = electionService;

		L.debug("Utworzono CommunicationService");
	}

	@Override
	public void setUp() throws BindException {
		L.debug("setUp");

		mainNode = nodesService.getMainNode();
		server.setUp(mainNode.getLocatorURI());

		setUpClients();

		setUpMessages();

		taskExecutor.execute(messageSender);
		taskExecutor.execute(messageReceiver);
		taskExecutor.execute(clientChecker);
		taskExecutor.execute(communicationChecker);
	}

	private void setUpClients() {
		L.debug("setUpClients");

		for (Node node : nodesService.getMainNode().getChildren()) {
			addClient(node);
			if (node.getState().getStateEnum() == NodeStateEnum.OFF)
				addClientToCheck(clients.get(node.getLocatorURI()));
		}
	}

	@Override
	public void addClient(Node node) {
		L.info("Dodaję kliena {} {}", node.getLocatorURI(), node.getNodeType());
		clients.put(node.getLocatorURI(), new Client(node.getLocatorURI(), node.getNodeType()));
	}

	@Override
	public void deleteClient(String locatorUri) {
		Client client = clients.get(locatorUri);
		if (client != null) {
			L.debug("Usuwam klienta " + client.toString());
			removeClientToCheck(client);
			clients.remove(locatorUri);
			client.disconnect();
		}
	}

	@Override
	public void changeClient(String oldLocatorURI, Node node) {
		deleteClient(oldLocatorURI);
		addClient(node);
	}

	private void setUpMessages() {
		L.debug("setUpMessages");

		List<MessageToSend> messagesTS = databaseService.getMessagesToSend();
		List<MessageReceived> messagesR = databaseService.getMessagesReceived();

		for (MessageToSend messageToSend : messagesTS) {
			Message message = (Message) Converter.xmlToObject(messageToSend.getValue());
			message.setMessageToSendDatabaseId(messageToSend.getId());
			messagesToSend.add(message);
		}

		for (MessageReceived messageReceived : messagesR) {
			Message message = (Message) Converter.xmlToObject(messageReceived.getValue());
			messagesReceived.add(message);
		}

	}

	@Override
	public void shutDown() {
		L.debug("shutDown");

		server.shutdownServer();
		messageSender.stop();
		messageReceiver.stop();
		clientChecker.stop();
		communicationChecker.stop();
	}

	@Override
	public Message getMessageToSend() {
		Message message = null;
		synchronized (messagesToSend) {
			message = messagesToSend.get();
		}

		return message;
	}

	@Override
	public HashMap<String, Client> getClients() {
		return clients;
	}

	@Override
	public void resendMessage(Message message) {
		// L.debug("Ponownie wysyłam wiadomość {}", message.toString());
		synchronized (messagesToSend) {
			messagesToSend.remove(message);
			messagesToSend.add(message);
			messageSender.wakeUp();
		}
	}

	@Override
	public void removeMessageToSend(Message message) {
		L.debug("Usuwam wiadomośc {}", message.toString());
		synchronized (messagesToSend) {
			databaseService.removeMessageToSend(message.getMessageToSendDatabaseId());
			messagesToSend.remove(message);
		}
	}

	@Override
	public void addClientToCheck(Client client) {
		L.debug("Dodaje {} do sprawdzenia", client.getLocatorURI());
		synchronized (clientsToCheck) {
			clientsToCheck.add(client);
			clientChecker.wakeUp();
		}
	}

	@Override
	public Message getReceivedMessage() {
		Message message = null;
		synchronized (messagesReceived) {
			message = messagesReceived.get();
		}
		return message;
	}

	@Override
	public void removeReceivedMessage(Message message) {
		databaseService.setMessageAsHandled(message.getMessageToSendDatabaseId());
		messagesReceived.remove(message);
	}

	@Override
	public void updateNodeState(String locatorURI, CNodeState nodeState) {
		nodesService.changeNodeState(locatorURI, nodeState);
	}

	@Override
	public Client getClientToCheck() {
		Client client = null;
		synchronized (clientsToCheck) {
			if (!clientsToCheck.isEmpty())
				client = clientsToCheck.getFirst();
		}
		return client;
	}

	@Override
	public void reAddClientToCheck(Client client) {
		synchronized (clientChecker) {
			clientsToCheck.remove(client);
			clientsToCheck.add(client);
		}
	}

	@Override
	public void addReceivedMessage(Message message) {
		L.info("Dodaje otrzymaną wiadomośc {}", message.toString());
		synchronized (messagesReceived) {
			databaseService.storeReceivedMessage(message);
			messagesReceived.add(message);
			messageReceiver.wakeUp();
		}
	}

	@Override
	public void removeClientToCheck(Client client) {
		L.debug("Usuwam {} do sprawdzenia", client.getLocatorURI());
		synchronized (clientsToCheck) {
			clientsToCheck.remove(client);
		}
	}

	@Override
	public void send(Message message) {
		L.debug("Wysyłam wiadomość {}", message);

		if (message.getURIFromPath() == null) {
			setPath(message);
		}

		synchronized (messagesToSend) {
			databaseService.storeMessageToSend(message);
			messagesToSend.add(message);
			messageSender.wakeUp();
		}
	}

	@Override
	public void setPath(Message message) {
		L.debug("Ustawiam ścieżkę {}", message);

		Node node = nodesService.getNode(message.getDestinationURI());

		message.addToPath(node.getLocatorURI());
		while (!node.getParent().equals(mainNode)) {
			node = node.getParent();
			message.addToPath(node.getLocatorURI());
		}
	}

	@Override
	public void sendConfigurations() {
		List<NodeChanges> changes = databaseService.getAllNodeChanges();
		ConfigurationMessage message;

		for (NodeChanges nodesChange : changes) {
			System.err.println("Tworzę ConfigurationMessage dla " + nodesChange.getUri());
			message = new ConfigurationMessage(nodesChange.getUri(), nodesChange.getNodeActions());
			send(message);
			nodesService.changeNodeUpToDate(nodesChange.getUri(), true);
			databaseService.deleteNodeChanges(nodesChange);
		}
	}

	@Override
	public void sendElectionConfiguration(Election election) {
		List<ElectionChanges> changes = databaseService.getAllElectionChages(election.getIdentificationField());
		ElectionConfigurationMessage message;

		for (ElectionChanges change : changes) {
			message = new ElectionConfigurationMessage(change.getUri(), change.getElectionIdentificationField(), change.getElectionActions());
			send(message);
			databaseService.deleteElectionChanges(change);
		}
		election.setStatus(ElectionStatus.SEND);
		election = databaseService.storeElection(election);
		mainWindow.refreshElection(election);
	}

	@Override
	public void sendVoterUpdate(String identificationField, Voter voter) {
		UpdateVoterMessage message;
		for (ElectionResidentVoter rVoter : voter.getElections()) {
			Election election = rVoter.getElectionNode().getElection();
			if (election.getStatus() != ElectionStatus.INPROGRESS || election.getStatus() != ElectionStatus.COMPLETED) {
				message = new UpdateVoterMessage(rVoter.getElectionNode().getNode().getLocatorURI(), identificationField, voter.toCVoter());
				send(message);
			}
		}
		for (ElectionNonResidentVoter nrVoter : voter.getNonResidentElections()) {
			if (nrVoter.getElection().getStatus() != ElectionStatus.INPROGRESS || nrVoter.getElection().getStatus() != ElectionStatus.COMPLETED) {
				for (ElectionNode node : nrVoter.getElection().getNodes()) {
					message = new UpdateVoterMessage(node.getNode().getLocatorURI(), identificationField, voter.toCVoter());
					send(message);
				}
			}
		}

	}

	@Override
	public void sendMemberUpdate(String identificationField, Member member) {
		UpdateMemberMessage message;
		for (AdministratorToken token : member.getTokens()) {
			Election election = token.getElectionNode().getElection();
			if (election.getStatus() != ElectionStatus.INPROGRESS || election.getStatus() != ElectionStatus.COMPLETED) {
				message = new UpdateMemberMessage(token.getElectionNode().getNode().getLocatorURI(), identificationField, member.toCMemberWihtoutAdministratorToken());
				send(message);
			}
		}
	}

	@Override
	public void verifyVoter(VerifyNonresidentVoterMessage message) {
		Boolean verified = databaseService.verifyVoter(message.getElectionIdentificationField(), message.getVoterIdentificationField());
		
		if (verified)
			electionService.addNonresidentVoterToElectionResults(message.getElectionIdentificationField());
			
		VerifyNonresidentVoterConfirmationMessage msg = new VerifyNonresidentVoterConfirmationMessage(message, verified);
		send(msg);
	}

	@Override
	public void changeElectionStatus(ElectionStatusMessage message) {
		Election election;
		ElectionNode node = databaseService.changeElectionNodeStatus(message.getElectionId(), message.getStatus(), message.getLocatorURI());
		election = node.getElection();
		if (node.getStatus() == ElectionStatus.INPROGRESS && node.getElection().getStatus() == ElectionStatus.SEND) {
			node.getElection().setStatus(ElectionStatus.INPROGRESS);
			electionService.initElectionResults(node.getElection());
			election = databaseService.storeElection(node.getElection());
			mainWindow.refreshElectionPage();
		} else if (node.getStatus() == ElectionStatus.COMPLETED) {
			Boolean completed = true;
			for (ElectionNode n : node.getElection().getNodes()) {
				if (n.getStatus() != ElectionStatus.COMPLETED) {
					completed = false;
					break;
				}
			}
			if (completed) {
				node.getElection().setStatus(ElectionStatus.COMPLETED);
				election = databaseService.storeElection(node.getElection());
				mainWindow.refreshElectionPage();
			}
		}
		mainWindow.refreshDlgElectionState(election);
	}

	@Override
	public void handleVotesPackage(VotesPackageMessage message) {
		electionService.handleVotesPackage(message);
	}

	@Override
	public void handleVotersPackage(VotersPackageMessage message) {
		electionService.handleVotersPackage(message);
	}
}
